import snippet from 'tui-code-snippet';
import {toInteger, clamp} from '../../util';
import {keyCodes} from '../../consts';

const INPUT_FILTER_REGEXP = /(-?)([0-9]*)[^0-9]*([0-9]*)/g;

/**
 * Range control class
 * @class
 * @ignore
 */
class Range {
    /**
     * @constructor
     * @extends {View}
     * @param {Object} rangeElements - Html resources for creating sliders
     *  @param {HTMLElement} rangeElements.slider - b
     *  @param {HTMLElement} [rangeElements.input] - c
     * @param {Object} options - Slider make options
     *  @param {number} options.min - min value
     *  @param {number} options.max - max value
     *  @param {number} options.value - default value
     *  @param {number} [options.useDecimal] - Decimal point processing.
     *  @param {number} [options.realTimeEvent] - Reflect live events.
     */
    constructor(rangeElements, options = {}) {
        this._value = options.value || 0;

        this.rangeElement = rangeElements.slider;
        this.rangeInputElement = rangeElements.input;

        this._drawRangeElement();

        this.rangeWidth = this._getRangeWidth();
        this._min = options.min || 0;
        this._max = options.max || 100;
        this._useDecimal = options.useDecimal;
        this._absMax = (this._min * -1) + this._max;
        this.realTimeEvent = options.realTimeEvent || false;

        this.eventHandler = {
            startChangingSlide: this._startChangingSlide.bind(this),
            stopChangingSlide: this._stopChangingSlide.bind(this),
            changeSlide: this._changeSlide.bind(this),
            changeSlideFinally: this._changeSlideFinally.bind(this),
            changeInput: this._changeValueWithInput.bind(this, false),
            changeInputFinally: this._changeValueWithInput.bind(this, true),
            changeInputWithArrow: this._changeValueWithInputKeyEvent.bind(this)
        };

        this._addClickEvent();
        this._addDragEvent();
        this._addInputEvent();
        this.value = options.value;
        this.trigger('change');
    }

    /**
     * Destroys the instance.
     */
    destroy() {
        this._removeClickEvent();
        this._removeDragEvent();
        this._removeInputEvent();
        this.rangeElement.innerHTML = '';
        snippet.forEach(this, (value, key) => {
            this[key] = null;
        });
    }

    /**
     * Set range max value and re position cursor
     * @param {number} maxValue - max value
     */
    set max(maxValue) {
        this._max = maxValue;
        this._absMax = (this._min * -1) + this._max;
        this.value = this._value;
    }

    get max() {
        return this._max;
    }

    /**
     * Get range value
     * @returns {Number} range value
     */
    get value() {
        return this._value;
    }

    /**
     * Set range value
     * @param {Number} value range value
     * @param {Boolean} fire whether fire custom event or not
     */
    set value(value) {
        value = this._useDecimal ? value : toInteger(value);

        const absValue = value - this._min;
        let leftPosition = (absValue * this.rangeWidth) / this._absMax;

        if (this.rangeWidth < leftPosition) {
            leftPosition = this.rangeWidth;
        }

        this.pointer.style.left = `${leftPosition}px`;
        this.subbar.style.right = `${this.rangeWidth - leftPosition}px`;

        this._value = value;
        if (this.rangeInputElement) {
            this.rangeInputElement.value = value;
        }
    }

    /**
     * event tirigger
     * @param {string} type - type
     */
    trigger(type) {
        this.fire(type, this._value);
    }

    /**
     * Calculate slider width
     * @returns {number} - slider width
     */
    _getRangeWidth() {
        const getElementWidth = element => toInteger(window.getComputedStyle(element, null).width);

        return getElementWidth(this.rangeElement) - getElementWidth(this.pointer);
    }

    /**
     * Make range element
     * @private
     */
    _drawRangeElement() {
        this.rangeElement.classList.add('tui-image-editor-range');

        this.bar = document.createElement('div');
        this.bar.className = 'tui-image-editor-virtual-range-bar';

        this.subbar = document.createElement('div');
        this.subbar.className = 'tui-image-editor-virtual-range-subbar';

        this.pointer = document.createElement('div');
        this.pointer.className = 'tui-image-editor-virtual-range-pointer';

        this.bar.appendChild(this.subbar);
        this.bar.appendChild(this.pointer);
        this.rangeElement.appendChild(this.bar);
    }

    /**
     * Add range input editing event
     * @private
     */
    _addInputEvent() {
        if (this.rangeInputElement) {
            this.rangeInputElement.addEventListener('keydown', this.eventHandler.changeInputWithArrow);
            this.rangeInputElement.addEventListener('keyup', this.eventHandler.changeInput);
            this.rangeInputElement.addEventListener('blur', this.eventHandler.changeInputFinally);
        }
    }

    /**
     * Remove range input editing event
     * @private
     */
    _removeInputEvent() {
        if (this.rangeInputElement) {
            this.rangeInputElement.removeEventListener('keydown', this.eventHandler.changeInputWithArrow);
            this.rangeInputElement.removeEventListener('keyup', this.eventHandler.changeInput);
            this.rangeInputElement.removeEventListener('blur', this.eventHandler.changeInputFinally);
        }
    }

    /**
     * change angle event
     * @param {object} event - key event
     * @private
     */
    _changeValueWithInputKeyEvent(event) {
        const {keyCode, target} = event;

        if ([keyCodes.ARROW_UP, keyCodes.ARROW_DOWN].indexOf(keyCode) < 0) {
            return;
        }

        let value = Number(target.value);

        value = this._valueUpDownForKeyEvent(value, keyCode);

        const unChanged = value < this._min || value > this._max;

        if (!unChanged) {
            const clampValue = clamp(value, this._min, this.max);
            this.value = clampValue;
            this.fire('change', clampValue, false);
        }
    }

    /**
     * value up down for input
     * @param {number} value - original value number
     * @param {number} keyCode - input event key code
     * @returns {number} value - changed value
     * @private
     */
    _valueUpDownForKeyEvent(value, keyCode) {
        const step = this._useDecimal ? 0.1 : 1;

        if (keyCode === keyCodes.ARROW_UP) {
            value += step;
        } else if (keyCode === keyCodes.ARROW_DOWN) {
            value -= step;
        }

        return value;
    }

    /**
     * change angle event
     * @param {boolean} isLast - Is last change
     * @param {object} event - key event
     * @private
     */
    _changeValueWithInput(isLast, event) {
        const {keyCode, target} = event;

        if ([keyCodes.ARROW_UP, keyCodes.ARROW_DOWN].indexOf(keyCode) >= 0) {
            return;
        }

        const stringValue = this._filterForInputText(target.value);
        const waitForChange = !stringValue || isNaN(stringValue);
        target.value = stringValue;

        if (!waitForChange) {
            let value = this._useDecimal ? Number(stringValue) : toInteger(stringValue);
            value = clamp(value, this._min, this.max);

            this.value = value;
            this.fire('change', value, isLast);
        }
    }

    /**
     * Add Range click event
     * @private
     */
    _addClickEvent() {
        this.rangeElement.addEventListener('click', this.eventHandler.changeSlideFinally);
    }

    /**
     * Remove Range click event
     * @private
     */
    _removeClickEvent() {
        this.rangeElement.removeEventListener('click', this.eventHandler.changeSlideFinally);
    }

    /**
     * Add Range drag event
     * @private
     */
    _addDragEvent() {
        this.pointer.addEventListener('mousedown', this.eventHandler.startChangingSlide);
    }

    /**
     * Remove Range drag event
     * @private
     */
    _removeDragEvent() {
        this.pointer.removeEventListener('mousedown', this.eventHandler.startChangingSlide);
    }

    /**
     * change angle event
     * @param {object} event - change event
     * @private
     */
    _changeSlide(event) {
        const changePosition = event.screenX;
        const diffPosition = changePosition - this.firstPosition;
        let touchPx = this.firstLeft + diffPosition;
        touchPx = touchPx > this.rangeWidth ? this.rangeWidth : touchPx;
        touchPx = touchPx < 0 ? 0 : touchPx;

        this.pointer.style.left = `${touchPx}px`;
        this.subbar.style.right = `${this.rangeWidth - touchPx}px`;

        const ratio = touchPx / this.rangeWidth;
        const resultValue = (this._absMax * ratio) + this._min;
        const value = this._useDecimal ? resultValue : toInteger(resultValue);
        const isValueChanged = this.value !== value;

        if (isValueChanged) {
            this.value = value;
            if (this.realTimeEvent) {
                this.fire('change', this._value, false);
            }
        }
    }

    _changeSlideFinally(event) {
        event.stopPropagation();
        if (event.target.className !== 'tui-image-editor-range') {
            return;
        }
        const touchPx = event.offsetX;
        const ratio = touchPx / this.rangeWidth;
        const value = (this._absMax * ratio) + this._min;
        this.pointer.style.left = `${ratio * this.rangeWidth}px`;
        this.subbar.style.right = `${(1 - ratio) * this.rangeWidth}px`;
        this.value = value;

        this.fire('change', value, true);
    }

    _startChangingSlide(event) {
        this.firstPosition = event.screenX;
        this.firstLeft = toInteger(this.pointer.style.left) || 0;

        document.addEventListener('mousemove', this.eventHandler.changeSlide);
        document.addEventListener('mouseup', this.eventHandler.stopChangingSlide);
    }

    /**
     * stop change angle event
     * @private
     */
    _stopChangingSlide() {
        this.fire('change', this._value, true);

        document.removeEventListener('mousemove', this.eventHandler.changeSlide);
        document.removeEventListener('mouseup', this.eventHandler.stopChangingSlide);
    }

    /**
     * Unnecessary string filtering.
     * @param {string} inputValue - origin string of input
     * @returns {string} filtered string
     * @private
     */
    _filterForInputText(inputValue) {
        return inputValue.replace(INPUT_FILTER_REGEXP, '$1$2$3');
    }
}

snippet.CustomEvents.mixin(Range);
export default Range;
